agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v5 3/7] Row pattern recognition patch (planner). 1148+ messages / 2 participants [nested] [flat]
* [PATCH v5 3/7] Row pattern recognition patch (planner). @ 2023-09-02 06:32 Tatsuo Ishii <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Tatsuo Ishii @ 2023-09-02 06:32 UTC (permalink / raw) --- src/backend/optimizer/plan/createplan.c | 23 ++++++++++++++++++----- src/backend/optimizer/plan/setrefs.c | 23 +++++++++++++++++++++++ src/include/nodes/plannodes.h | 15 +++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 34ca6d4ac2..e3c07ded65 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -286,9 +286,10 @@ static WindowAgg *make_windowagg(List *tlist, Index winref, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, int frameOptions, Node *startOffset, Node *endOffset, Oid startInRangeFunc, Oid endInRangeFunc, - Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, - List *runCondition, List *qual, bool topWindow, - Plan *lefttree); + Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition, + RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp, List *defineClause, + List *defineInitial, + List *qual, bool topWindow, Plan *lefttree); static Group *make_group(List *tlist, List *qual, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, Plan *lefttree); @@ -2698,6 +2699,11 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) wc->inRangeAsc, wc->inRangeNullsFirst, wc->runCondition, + wc->rpSkipTo, + wc->patternVariable, + wc->patternRegexp, + wc->defineClause, + wc->defineInitial, best_path->qual, best_path->topwindow, subplan); @@ -6601,8 +6607,10 @@ make_windowagg(List *tlist, Index winref, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, int frameOptions, Node *startOffset, Node *endOffset, Oid startInRangeFunc, Oid endInRangeFunc, - Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, - List *runCondition, List *qual, bool topWindow, Plan *lefttree) + Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst, List *runCondition, + RPSkipTo rpSkipTo, List *patternVariable, List *patternRegexp, List *defineClause, + List *defineInitial, + List *qual, bool topWindow, Plan *lefttree) { WindowAgg *node = makeNode(WindowAgg); Plan *plan = &node->plan; @@ -6628,6 +6636,11 @@ make_windowagg(List *tlist, Index winref, node->inRangeAsc = inRangeAsc; node->inRangeNullsFirst = inRangeNullsFirst; node->topWindow = topWindow; + node->rpSkipTo = rpSkipTo, + node->patternVariable = patternVariable; + node->patternRegexp = patternRegexp; + node->defineClause = defineClause; + node->defineInitial = defineInitial; plan->targetlist = tlist; plan->lefttree = lefttree; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 97fa561e4e..2ed00b5d41 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2456,6 +2456,29 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset) NRM_EQUAL, NUM_EXEC_QUAL(plan)); + /* + * Modifies an expression tree in each DEFINE clause so that all Var + * nodes reference outputs of a subplan. + */ + if (IsA(plan, WindowAgg)) + { + WindowAgg *wplan = (WindowAgg *) plan; + + foreach(l, wplan->defineClause) + { + TargetEntry *tle = (TargetEntry *) lfirst(l); + + tle->expr = (Expr *) + fix_upper_expr(root, + (Node *) tle->expr, + subplan_itlist, + OUTER_VAR, + rtoffset, + NRM_EQUAL, + NUM_EXEC_QUAL(plan)); + } + } + pfree(subplan_itlist); } diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 1b787fe031..19815a98bb 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1096,6 +1096,21 @@ typedef struct WindowAgg /* nulls sort first for in_range tests? */ bool inRangeNullsFirst; + /* Row Pattern Recognition AFTER MACH SKIP clause */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + + /* Row Pattern PATTERN variable name (list of String) */ + List *patternVariable; + + /* Row Pattern RPATTERN regular expression quantifier ('+' or ''. list of String) */ + List *patternRegexp; + + /* Row Pattern DEFINE clause (list of TargetEntry) */ + List *defineClause; + + /* Row Pattern DEFINE variable initial names (list of String) */ + List *defineInitial; + /* * false for all apart from the WindowAgg that's closest to the root of * the plan -- 2.25.1 ----Next_Part(Sat_Sep__2_15_52_35_2023_273)-- Content-Type: Text/X-Patch; charset=us-ascii Content-Transfer-Encoding: 7bit Content-Disposition: inline; filename="v5-0004-Row-pattern-recognition-patch-executor.patch" ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --tbByXeXYMjI7URMk-- ^ permalink raw reply [nested|flat] 1148+ messages in thread
* [PATCH v2 1/3] tell client when prep stmts are deallocated @ 2026-05-28 20:42 Nathan Bossart <[email protected]> 0 siblings, 0 replies; 1148+ messages in thread From: Nathan Bossart @ 2026-05-28 20:42 UTC (permalink / raw) --- src/backend/commands/prepare.c | 30 +++++++++++++ src/include/libpq/pqcomm.h | 2 +- src/include/libpq/protocol.h | 1 + src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-connect.c | 30 +++++++++++++ src/interfaces/libpq/fe-protocol3.c | 43 +++++++++++++++++++ src/interfaces/libpq/fe-trace.c | 10 +++++ src/interfaces/libpq/libpq-fe.h | 6 +++ src/interfaces/libpq/libpq-int.h | 2 + .../modules/libpq_pipeline/libpq_pipeline.c | 12 +++--- .../libpq_pipeline/traces/prepared.trace | 1 + 11 files changed, 131 insertions(+), 7 deletions(-) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 876aad2100a..4ca85b10f9e 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -26,6 +26,9 @@ #include "commands/explain_state.h" #include "commands/prepare.h" #include "funcapi.h" +#include "libpq/libpq-be.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_coerce.h" #include "parser/parse_collate.h" @@ -512,6 +515,27 @@ DeallocateQuery(DeallocateStmt *stmt) DropAllPreparedStatements(); } +/* + * Tell the client that a prepared statement has been deallocated. Pass an + * empty string to indicate that all statements were deallocated. + * + * This is only sent to clients that are using protocol version 3.3 or later. + */ +static void +SendStmtDeallocMsg(const char *name) +{ + StringInfoData buf; + + if (whereToSendOutput != DestRemote) + return; + if (!MyProcPort || MyProcPort->proto < PG_PROTOCOL(3, 3)) + return; + + pq_beginmessage(&buf, PqMsg_PrepStmtDeallocated); + pq_sendstring(&buf, name); + pq_endmessage(&buf); +} + /* * Internal version of DEALLOCATE * @@ -530,6 +554,9 @@ DropPreparedStatement(const char *stmt_name, bool showError) /* Release the plancache entry */ DropCachedPlan(entry->plansource); + /* Alert the client */ + SendStmtDeallocMsg(entry->stmt_name); + /* Now we can remove the hash table entry */ hash_search(prepared_queries, entry->stmt_name, HASH_REMOVE, NULL); } @@ -548,6 +575,9 @@ DropAllPreparedStatements(void) if (!prepared_queries) return; + /* Alert the client */ + SendStmtDeallocMsg(""); + /* walk over cache */ hash_seq_init(&seq, prepared_queries); while ((entry = hash_seq_search(&seq)) != NULL) diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h index a29c9c94d79..28e7944cdf4 100644 --- a/src/include/libpq/pqcomm.h +++ b/src/include/libpq/pqcomm.h @@ -92,7 +92,7 @@ is_unixsock_path(const char *path) * The earliest and latest frontend/backend protocol version supported. */ #define PG_PROTOCOL_EARLIEST PG_PROTOCOL(3,0) -#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,2) +#define PG_PROTOCOL_LATEST PG_PROTOCOL(3,3) /* * Reserved protocol numbers, which have special semantics: diff --git a/src/include/libpq/protocol.h b/src/include/libpq/protocol.h index eae8f0e7238..7ea331f7210 100644 --- a/src/include/libpq/protocol.h +++ b/src/include/libpq/protocol.h @@ -53,6 +53,7 @@ #define PqMsg_FunctionCallResponse 'V' #define PqMsg_CopyBothResponse 'W' #define PqMsg_ReadyForQuery 'Z' +#define PqMsg_PrepStmtDeallocated 'i' #define PqMsg_NoData 'n' #define PqMsg_PortalSuspended 's' #define PqMsg_ParameterDescription 't' diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 1e3d5bd5867..effd73ca3e6 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -211,3 +211,4 @@ PQdefaultAuthDataHook 208 PQfullProtocolVersion 209 appendPQExpBufferVA 210 PQgetThreadLock 211 +PQaddPrepStmtDeallocCallback 212 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 4272d386e64..5e41c21c6f6 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -8374,6 +8374,11 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn, *result = PG_PROTOCOL(3, 2); return true; } + if (strcmp(value, "3.3") == 0) + { + *result = PG_PROTOCOL(3, 3); + return true; + } libpq_append_conn_error(conn, "invalid %s value: \"%s\"", context, value); @@ -8426,3 +8431,28 @@ PQgetThreadLock(void) Assert(pg_g_threadlock); return pg_g_threadlock; } + +/* + * Adds a prepared statement deallocation callback to the connection's list of + * callbacks. These are invoked when the server sends us + * PqMsg_PrepStmtDeallocated messages. + */ +bool +PQaddPrepStmtDeallocCallback(PGconn *conn, PQprepStmtDeallocCallback cb) +{ + if (!conn) + return false; + + /* Add to end to preserve registration order */ + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (conn->prepStmtDeallocCallbacks[i]) + continue; + + conn->prepStmtDeallocCallbacks[i] = cb; + return true; + } + + libpq_append_conn_error(conn, "maximum number of prepared statement deallocation callbacks already registered"); + return false; +} diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 840e018cd18..0407d10362d 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -61,6 +61,32 @@ static size_t build_startup_packet(const PGconn *conn, char *packet, const PQEnvironmentOption *options); +/* + * Attempt to read a PrepStmtDeallocated message and invoke the connection's + * registered callbacks. This is possible in several places, so we break it + * out as a subroutine. + * + * Entry: 'i' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message and invoked callbacks, or + * EOF if not enough data. + */ +static int +getPrepStmtDeallocated(PGconn *conn) +{ + if (pqGets(&conn->workBuffer, conn)) + return EOF; + + for (int i = 0; i < lengthof(conn->prepStmtDeallocCallbacks); i++) + { + if (!conn->prepStmtDeallocCallbacks[i]) + break; + + (conn->prepStmtDeallocCallbacks[i]) (conn, conn->workBuffer.data); + } + + return 0; +} + /* * parseInput: if appropriate, parse input data from backend * until input is exhausted or a stopping state is reached. @@ -184,6 +210,11 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; } + else if (id == PqMsg_PrepStmtDeallocated) + { + if (getPrepStmtDeallocated(conn)) + return; + } else { /* Any other case is unexpected and we summarily skip it */ @@ -305,6 +336,10 @@ pqParseInput3(PGconn *conn) if (getParameterStatus(conn)) return; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return; + break; case PqMsg_BackendKeyData: /* @@ -1905,6 +1940,10 @@ getCopyDataMessage(PGconn *conn) if (getParameterStatus(conn)) return 0; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + return 0; + break; case PqMsg_CopyData: return msgLength; case PqMsg_CopyDone: @@ -2409,6 +2448,10 @@ pqFunctionCall3(PGconn *conn, Oid fnid, if (getParameterStatus(conn)) continue; break; + case PqMsg_PrepStmtDeallocated: + if (getPrepStmtDeallocated(conn)) + continue; + break; default: /* The backend violates the protocol. */ libpq_append_conn_error(conn, "protocol error: id=0x%x", id); diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index c348b08c39b..e9f734187a2 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -543,6 +543,13 @@ pqTraceOutput_ParameterStatus(FILE *f, const char *message, int *cursor) pqTraceOutputString(f, message, cursor, false); } +static void +pqTraceOutput_PrepStmtDeallocated(FILE *f, const char *message, int *cursor) +{ + fprintf(f, "PrepStmtDeallocated\t"); + pqTraceOutputString(f, message, cursor, false); +} + static void pqTraceOutput_ParameterDescription(FILE *f, const char *message, int *cursor, bool regress) { @@ -793,6 +800,9 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer) else pqTraceOutput_ParameterStatus(conn->Pfdebug, message, &logCursor); break; + case PqMsg_PrepStmtDeallocated: + pqTraceOutput_PrepStmtDeallocated(conn->Pfdebug, message, &logCursor); + break; case PqMsg_ParameterDescription: pqTraceOutput_ParameterDescription(conn->Pfdebug, message, &logCursor, regress); break; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 8ecb9b4a4c7..c57bb8806cf 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -486,6 +486,12 @@ typedef void (*pgthreadlock_t) (int acquire); extern pgthreadlock_t PQregisterThreadLock(pgthreadlock_t newhandler); extern pgthreadlock_t PQgetThreadLock(void); +/* callbacks for prepared statement deallocation notifications */ +typedef void (*PQprepStmtDeallocCallback) (PGconn *conn, const char *name); + +extern bool PQaddPrepStmtDeallocCallback(PGconn *conn, + PQprepStmtDeallocCallback cb); + /* === in fe-trace.c === */ extern void PQtrace(PGconn *conn, FILE *debug_port); extern void PQuntrace(PGconn *conn); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 461b39620c3..7eca941ddcc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -532,6 +532,8 @@ struct pg_conn void (*cleanup_async_auth) (PGconn *conn); pgsocket altsock; /* alternative socket for client to poll */ + /* Callbacks for prep stmt deallocs (16 ought to be enough for anybody) */ + PQprepStmtDeallocCallback prepStmtDeallocCallbacks[16]; /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ee3e2ec7570..b61f33e7cd9 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn) Assert(max_protocol_version_index >= 0); /* - * Test default protocol_version (GREASE - should negotiate down to 3.2) + * Test default protocol_version (GREASE - should negotiate down to 3.3) */ vals[max_protocol_version_index] = ""; conn = PQconnectdbParams(keywords, vals, false); @@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); @@ -1423,7 +1423,7 @@ test_protocol_version(PGconn *conn) PQfinish(conn); /* - * Test max_protocol_version=latest. 'latest' currently means '3.2'. + * Test max_protocol_version=latest. 'latest' currently means '3.3'. */ vals[max_protocol_version_index] = "latest"; conn = PQconnectdbParams(keywords, vals, false); @@ -1433,8 +1433,8 @@ test_protocol_version(PGconn *conn) PQerrorMessage(conn)); protocol_version = PQfullProtocolVersion(conn); - if (protocol_version != 30002) - pg_fatal("expected 30002, got %d", protocol_version); + if (protocol_version != 30003) + pg_fatal("expected 30003, got %d", protocol_version); PQfinish(conn); diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index aeb5de109e0..5d36fb0056d 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -7,6 +7,7 @@ B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 655 B 5 ReadyForQuery I F 16 Close S "select_one" F 4 Sync +B 15 PrepStmtDeallocated "select_one" B 4 CloseComplete B 5 ReadyForQuery I F 16 Describe S "select_one" -- 2.50.1 (Apple Git-155) --wfzLmV/Wl0ehOJBp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v2-0002-stop-using-PQfn-in-libpq-s-LO-interface.patch ^ permalink raw reply [nested|flat] 1148+ messages in thread
end of thread, other threads:[~2026-05-28 20:42 UTC | newest] Thread overview: 1148+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2023-09-02 06:32 [PATCH v5 3/7] Row pattern recognition patch (planner). Tatsuo Ishii <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]> 2026-05-28 20:42 [PATCH v2 1/3] tell client when prep stmts are deallocated Nathan Bossart <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox